// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: xguo

#pragma once

#include <iomanip>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include <nlohmann/json.hpp>
#include <openssl/ec.h>

#include "coin2/base/base64.h"
#include "coin2/base/time.h"
#include "coin2/exchange/base/api_base/asio_http_context.h"
#include "coin2/exchange/base/api_base/base_auth.h"
#include "coin2/exchange/base/api_util/api_signature.h"
#include "coin2/exchange/base/api_util/auth_key.h"
#include "coin2/exchange/base/api_util/json_writer.h"
#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order_spec.h"
#include "starkware/algebra/big_int.h"
#include "starkware/crypto/ecdsa.h"

/*
 def generate_now_iso():
     return datetime.utcnow().strftime(
         '%Y-%m-%dT%H:%M:%S.%f',
     )[:-3] + 'Z'
*/
inline const std::string GenerateNowIso() {
  auto s = Iso8601FromTimestamp(GetCurrentTimestamp());
  s = s.substr(0, 23) + "Z";
  return s;
}

class DydxBaseAuth {
 public:
  virtual ~DydxBaseAuth() = default;
};

class DydxWsAuth : public DydxBaseAuth {
 public:
  explicit DydxWsAuth(const std::string& key_filepath) : key_(AuthKey::FromFile(key_filepath)) {
    secret_key_ = impl::urlsafe_base64_decode(key_.secret_key());
  }

  const std::string CreateSignature() const {
    auto ts = GenerateNowIso();
    std::string cred = ts + "GET" + "/ws/accounts";
    std::string signa = ApiSignatureUtil::GetUrlSafeSignatureByHmacSHA256(cred, secret_key_);
    nlohmann::json msg = nlohmann::json::object();

    msg["type"] = "subscribe";
    msg["channel"] = "v3_accounts";
    msg["accountNumber"] = "0";
    msg["apiKey"] = key_.access_key();
    msg["passphrase"] = key_.password();
    msg["timestamp"] = ts;
    msg["signature"] = signa;
    return msg.dump();
  }

 protected:
  AuthKey key_;
  std::string secret_key_;
};

inline int char2int(char input) {
  if(input >= '0' && input <= '9') {
    return input - '0';
  } else if(input >= 'A' && input <= 'F') {
    return input - 'A' + 10;
  } else if(input >= 'a' && input <= 'f') {
    return input - 'a' + 10;
  }
  throw std::invalid_argument("Invalid input string");
}

// This function assumes src to be a zero terminated sanitized string with
// an even number of [0-9a-f] characters, and target to be sufficiently large
inline void hex2bin(const char* src, char* target) {
  while(src[0] && src[1]) {
    *(target--) = char2int(src[0])*16 + char2int(src[1]);
    src += 2;
  }
}

class DydxRestAuth : public DydxBaseAuth, public BaseRestAuth {
 public:
  explicit DydxRestAuth(
      const std::string& key_filepath,
      const std::string& host,
      int32_t port,
      const std::string& bind_ip)
      : key_(AuthKey::FromFile(key_filepath)),
        host_(host),
        port_(std::to_string(port)),
        bind_ip_(bind_ip) {
    secret_key_ = impl::urlsafe_base64_decode(key_.secret_key());

    std::string pkey;

    pkey = key_.GetValue("private_key").value();
    CHECK(pkey[0] == '0' && (pkey[1] == 'x' || pkey[1] == 'X')) << pkey;
    CHECK_EQ(pkey.size(), 64+2);
    hex2bin(pkey.data()+2, reinterpret_cast<char*>(&private_key_)+31);
    LOG(INFO) << "private key str: " << pkey;

    pkey = key_.GetValue("public_key").value();
    CHECK(pkey[0] == '0' && (pkey[1] == 'x' || pkey[1] == 'X')) << pkey;
    CHECK_EQ(pkey.size(), 64+2);
    hex2bin(pkey.data()+2, reinterpret_cast<char*>(&pub_key_)+31);
    LOG(INFO) << "public key str: " << pkey;

    LOG(INFO) << "secret key: " << key_.secret_key();
    LOG(INFO) << "private key: " << private_key_;
    LOG(INFO) << "public key: " << pub_key_;
  }

  std::string GetSignature(
      const std::string& timestamp,
      const std::string& method,
      const std::string& url_path,
      const std::string& body) const {
    std::string cred = timestamp + method + url_path + body;
    return ApiSignatureUtil::GetUrlSafeSignatureByHmacSHA256(cred, secret_key_);
  }

  /*
  {
  "iso_timestamp": "2021-11-02T05:45:24.954Z",
  "method": "GET",
  "request_path": "/v3/accounts",
  "data": {},
  "msg": "2021-11-02T05:45:24.954ZGET/v3/accounts",
  "result": "H-EBfPhYfSa6-64eHfVGl1_zRVDYgWoRcOH9JxI3z_0="
  }
  */
  std::vector<std::pair<std::string, std::string>> GetHeaderSet(
      const std::string& method,
      const std::string& url_path,
      const std::string& body) const {
    std::vector<std::pair<std::string, std::string>> out;
    std::string ts = GenerateNowIso();

    std::string signa = GetSignature(ts, method, url_path, body);
    out.push_back(std::make_pair("DYDX-SIGNATURE", signa));
    out.push_back(std::make_pair("DYDX-API-KEY", key_.access_key()));
    out.push_back(std::make_pair("DYDX-PASSPHRASE", key_.password()));
    out.push_back(std::make_pair("DYDX-TIMESTAMP", ts));
    out.push_back(std::make_pair("DYDX-ACCOUNT-NUMBER", "0"));
    return out;
  }

  AsioHttpContext* CreateRequestByGetWithSignature(
      const std::string& url_path,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    std::string post_body = builder->ComputePostBody();
    CHECK(post_body.empty());
    std::string target = url_path;

    return AsioHttpContext::NewGet(
        host_,
        port_,
        bind_ip_,
        target,
        GetHeaderSet("GET", url_path, ""));
  }

  AsioHttpContext* CreateRequestByPostWithSignature(
      const std::string& url_path,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    CHECK(query_string.empty());
    std::string post_body = builder->ComputePostBody();
    std::string target = url_path;

    return AsioHttpContext::NewPost(
        host_,
        port_,
        bind_ip_,
        target,
        GetHeaderSet("POST", url_path, post_body),
        post_body);
  }

  AsioHttpContext* CreateRequestByDeleteWithSignature(
      const std::string& url_path,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    std::string post_body = builder->ComputePostBody();
    std::string target = url_path;
    return AsioHttpContext::NewDelete(
        host_,
        port_,
        bind_ip_,
        target,
        GetHeaderSet("DELETE", url_path, post_body),
        post_body);
  }

  AsioHttpContext* CreateRequestByPutWithSignature(
      const std::string& address,
      UrlParamsBuilder* builder) override {
    return nullptr;
  }

  auto GetPrivKey() const {
    return private_key_;
  }

  auto GetPubKey() const {
    return pub_key_;
  }

 protected:
  AuthKey key_;
  std::string host_;
  std::string port_;
  std::string bind_ip_;
  std::string secret_key_;
  starkware::BigInt<4> private_key_;
  starkware::BigInt<4> pub_key_;
};

struct StarkwareOrder {
  using OrderSpec = coin2::exchange::base::order::OrderSpec;
  using ValueType = starkware::BigInt<4>;

  ValueType asset_id_synthetic;   // done
  ValueType asset_id_collateral;  // done
  ValueType asset_id_fee;         // done
  bool is_buy_synthetic;          // done

  uint64_t quantums_amount_synthetic;   // done
  uint64_t quantums_amount_collateral;  // done
  uint64_t quantums_amount_fee;         // done check
  uint64_t nonce;
  uint64_t position_id;
  uint64_t expiration_epoch_hours;
  ValueType pub_key;
  nlohmann::json json;

  explicit StarkwareOrder(const OrderSpec& order_spec);
  int CalculateHash(const EC_GROUP* group, BIGNUM* res);
  std::string Sign(const ValueType&);
  std::string SignRpc(const ValueType&);
};
